/*
 * Copyright (C) 2008 GeoScheduler Team, as stated on <http://www.geoscheduler.org/authors>.
 * 
 * This file is part of GeoScheduler.
 * 
 * GeoScheduler is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GeoScheduler is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with GeoScheduler. If not, see <http://www.gnu.org/licenses/>.
 */
package org.geoscheduler.android.rest;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.geoscheduler.android.GSContext;
import org.geoscheduler.android.exceptions.GSContextNotInitializedException;
import org.geoscheduler.android.exceptions.GSRestFailedException;
import org.geoscheduler.android.exceptions.GSRestReturnedErrorException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.util.Log;

/**
 * Ancestor of various REST client components.
 * 
 * Provides common functionality necessary for low-level communication with REST API.
 * 
 * FIXME this must definitely be cleaned up!
 *
 * @author srakyi
 */
public abstract class AbstractRestClient {
	public static final String LOGTAG = AbstractRestClient.class.getName();
	
	/**
	 * Prepares HTTP POST request (with given data in body) to be executed.
	 * 
	 * @param relativeResourcePath	Relative path of resource we're calling.
	 * @param data					Data to be sent to given resource.
	 * 
	 * @return	{@link JSONObject} with returned data (prospective error message is extracted and transformed to exception).
	 * 
	 * @throws GSRestFailedException
	 * @throws GSRestReturnedErrorException
	 * @throws GSContextNotInitializedException
	 */
	protected JSONObject restPostCall(String relativeResourcePath, JSONObject data) throws GSRestFailedException, GSRestReturnedErrorException, GSContextNotInitializedException {
		HttpPost post = buildPost(relativeResourcePath, data);
		
		return extractValue(executeRequest(post));
	}
	
	/**
	 * Same as restPostCall(String, JSONObject), but extracts {@link JSONArray} from returned response.
	 */
	protected JSONArray restPostCallArray(String relativeResourcePath, JSONObject data) throws GSRestFailedException, GSRestReturnedErrorException, GSContextNotInitializedException {
		HttpPost post = buildPost(relativeResourcePath, data);
		
		return extractValueArray(executeRequest(post));
	}

	/**
	 * Prepares HTTP post to be used by HTTP client.
	 * 
	 * @param relativeResourcePath	Relative path of resource we're calling.
	 * @param data					Data to be sent to given resource.
	 * 
	 * @return	{@link HttpPost} ready to use.
	 * 
	 * @throws GSContextNotInitializedException
	 * @throws GSRestFailedException
	 */
	private HttpPost buildPost(String relativeResourcePath, JSONObject data) throws GSContextNotInitializedException, GSRestFailedException {
		HttpPost post = null;
		try {
			post = new HttpPost(GSContext.getPref(GSContext.PREFS_SERVER_URL) + relativeResourcePath);
			post.addHeader("Content-Type", "application/json");
			
			post.setEntity(new StringEntity(data.toString()));
		} catch (IllegalArgumentException e) {
			Log.e(LOGTAG, "Error in URI syntax of HTTP client, used URI: '" + GSContext.getPref(GSContext.PREFS_SERVER_URL) + relativeResourcePath +"'", e);
			throw new GSRestFailedException(e);
		} catch (UnsupportedEncodingException e) {
			Log.e(LOGTAG, "UnsupportedEncodingExcpetion during HTTP request construction.", e);
			throw new GSRestFailedException(e);
		}
		return post;
	}
	
	/**
	 * Prepares HTTP GET request to be executed.
	 * 
	 * @param relativeResourcePath	Relative path of resource we're calling.
	 * 
	 * @return	{@link JSONObject} with returned data (prospective error message is extracted and transformed to exception).
	 * 
	 * @throws GSRestFailedException
	 * @throws GSRestReturnedErrorException
	 * @throws GSContextNotInitializedException
	 */
	protected JSONObject restGetCall(String relativeResourcePath) throws GSRestFailedException, GSRestReturnedErrorException, GSContextNotInitializedException {
		return restGetCall(GSContext.getPref(GSContext.PREFS_SERVER_URL).toString(), relativeResourcePath);
	}

	/**
	 * Similar to restGetCall(String), but this one uses given baseURL instead of the one stored in preferences.
	 * 
	 * @param baseURL				Base URL of server to be used.
	 * @param relativeResourcePath	Relative path of resource we're calling.
	 * 
	 * @return 	{@link JSONObject} with returned data (prospective error message is extracted and transformed to exception).
	 * 
	 * @throws GSRestFailedException
	 * @throws GSRestReturnedErrorException
	 * @throws GSContextNotInitializedException
	 */
	protected JSONObject restGetCall(String baseURL, String relativeResourcePath) throws GSRestFailedException, GSRestReturnedErrorException, GSContextNotInitializedException {
		HttpGet get = null;
		try {
			get = new HttpGet(baseURL + relativeResourcePath);
		} catch (IllegalArgumentException e) {
			Log.e(LOGTAG, "Error in URI syntax of HTTP client, used URI: '" + GSContext.getPref(GSContext.PREFS_SERVER_URL) + relativeResourcePath +"'", e);
			throw new GSRestFailedException(e);
		}
		
		return extractValue(executeRequest(get));
	}
	
	/**
	 * Private method that takes care of HTTP calls.
	 * 
	 * @param request	Request to be executed.
	 * 
	 * @return	{@link JSONObject} with returned data (prospective error message is extracted and transformed to exception).
	 * 
	 * @throws GSRestFailedException
	 * @throws GSRestReturnedErrorException
	 */
	private String executeRequest(HttpUriRequest request) throws GSRestFailedException, GSRestReturnedErrorException {
		DefaultHttpClient htc = new DefaultHttpClient();
		
		HttpResponse resp = null;
		try {
			resp = htc.execute(request);
		} catch (Exception e) {
			Log.e(LOGTAG, "Execution of HTTP request failed.", e);
			throw new GSRestFailedException(e);
		}
			
		HttpEntity ent = resp.getEntity();
		
		String encoding = null;
		if (ent.getContentEncoding() != null) {
			encoding = ent.getContentEncoding().getName();
		} else {
			encoding = "UTF-8";
		}
		
		try {
			InputStream is;
			is = ent.getContent();
		
			StringBuilder sb = new StringBuilder();
			byte[] buff = new byte[512];
			while (is.read(buff) > -1) {
				// TODO what if buffer ends in the middle of letter? Can that happen?
				sb.append(new String(buff, encoding));
			}
			
			return sb.toString();
		} catch (IOException e) {
			Log.e(LOGTAG, "HTTP response parsing failed.", e);
			throw new GSRestFailedException(e);
		}
	}
	
	/**
	 * Processes server response and extracts returned value or error message.
	 * 
	 * @param response	Response as received from server.
	 * 
	 * @return	Real data extracted from response.
	 * 
	 * @throws GSRestFailedException
	 * @throws GSRestReturnedErrorException
	 */
	protected JSONObject extractValue(String response) throws GSRestFailedException, GSRestReturnedErrorException {
		try {
			JSONObject respJS = new JSONObject(response);
			
			if ("OK".equals(respJS.getString("result"))) {
				return respJS.getJSONObject("value");
			} else {
				JSONObject msgJS = respJS.getJSONObject("value");
				throw new GSRestReturnedErrorException(msgJS.getString("type"));
			}
		} catch (JSONException e) {
			Log.e(LOGTAG, "JSONException raised during JSON response unmarshalling.", e);
			throw new GSRestFailedException(e);
		}
	}
	
	/**
	 * Same as extractValue(String), but this one extracts value as {@link JSONArray}.
	 * 
	 * FIXME duplicate code (!)
	 */
	protected JSONArray extractValueArray(String response) throws GSRestFailedException, GSRestReturnedErrorException {
		try {
			JSONObject respJS = new JSONObject(response);
			
			if ("OK".equals(respJS.getString("result"))) {
				return respJS.getJSONArray("value");
			} else {
				JSONObject msgJS = respJS.getJSONObject("value");
				throw new GSRestReturnedErrorException(msgJS.getString("type"));
			}
		} catch (JSONException e) {
			Log.e(LOGTAG, "JSONException raised during JSON response unmarshalling.", e);
			throw new GSRestFailedException(e);
		}
	}
	
	/**
	 * Wrapper for general marshaller. It basically only cleans up thrown exception.
	 * 
	 * @param obj	Object to be marshalled.
	 * 
	 * @return	Representation of obj in {@link JSONObject}. 
	 * 
	 * @throws GSRestFailedException
	 */
	protected JSONObject marshall(Object obj) throws GSRestFailedException {
		try {
			return GSContext.getMarshaller().marshall(obj, new JSONObject());
		} catch (JSONException e) {
			Log.e(LOGTAG, "JSONException raised during Account marshalling.", e);
			throw new GSRestFailedException(e);
		}
	}

	/**
	 * Wrapper for general marshaller. It basically only cleans up thrown exception.
	 * 
	 * @param jsonObj	{@link JSONObject} representation of object.
	 * 
	 * @return	Unmarshalled instance.
	 * 
	 * @throws GSRestFailedException
	 */
	protected <T> T unmarshall(T instance, JSONObject jsonObj) throws GSRestFailedException {
		try {
			return GSContext.getMarshaller().unmarshall(instance, jsonObj);
		} catch (JSONException e) {
			Log.e(LOGTAG, "JSONException raised during returned Token unmarshalling.", e);
			throw new GSRestFailedException(e);
		}
	}
	
	/**
	 * Same as unmarshall(T, JSONObject), but this one unmarshalls JSNOArray.
	 */
	protected <T> List<T> unmarshall(T instance, JSONArray jsonArr) throws GSRestFailedException {
		try {
			return GSContext.getMarshaller().unmarshall(instance, jsonArr);
		} catch (JSONException e) {
			Log.e(LOGTAG, "JSONException raised during returned Token unmarshalling.", e);
			throw new GSRestFailedException(e);
		}
	}
}
